// 基类 调度
class Vue{
    constructor(options) { // 构造类时，会将实参传进来，我们可以获取到
        this.$el = options.el; // 获取到挂载点
        this.$data = options.data; // 获取到绑定的数据
        let computed = options.computed; // 获取计算属性
        let methods = options.methods; // 获取方法
        // 接下来需要判断这个根元素存在与否，存在则编译模板
        if (this.$el) {
            // 实现双向绑定的关键：数据劫持
            // 将数据全部转化成Object.defineProperty来定义
            new Observe(this.$data);
            for (let key in computed) { // 有依赖关系
                Object.defineProperty(this.$data, key, {
                    get:() => { // 箭头函数是为了this指向
                        return computed[key].call(this); 
                    }
                })
            }
            for (let key in methods) {
                Object.defineProperty(this, key, {
                    get(){ // 箭头函数是为了this指向
                        return methods[key]; 
                    }
                })
            }
            // 把数据上获取操作 vm 代理到 vm.$data => vm.info.name = vm.$data.info.name
            this.proxyVm(this.$data);
            console.log(this.$data)
            // 编译模板
            new Compiler(this.$el, this);
        }
    }
    proxyVm(data) { // 代理操作 => 也就是去掉$data进行取值
        for(let key in data) {
            Object.defineProperty(this, key, { 
                get() {
                    return data[key]; // 进行了转化操作
                },
                set(newVal) { // 设置代理方法
                    data[key] = newVal; // 每次更新值，将新值赋予，少掉的话，会使得无法实时更新
                }
            })
        }
    }
}
// 编译模板
class Compiler{
    constructor(el, vm) {
        this.vm = vm; // 便于在方法内使用，而不再通过传参的形式
        // 判断el属性是不是元素，不是则获取,这一步获取到了挂载点的div
        // document.querySelector("#demo"); 获取文档中 id="demo" 的元素
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        // 把当前节点中的元素 获取到 再放到内存中
        // 原因：如果我们逐个对绑定数据的dom进行替换，无疑会消耗性能，换成在内存中完成替换，再进行一次渲染，可以大大提升性能
        let fragment = this.node2fragment(this.el);
        // 之后需要将节点中的内容进行替换
        // 编译模板 用数据编译
        this.compile(fragment);
        // 将替换后的内容渲染到页面上
        this.el.appendChild(fragment);
    }
    isElementNode(node) { // 判断是不是元素节点
        // nodeType 如果该节点是元素节点则返回1，如果是属性节点则返回2
        return node.nodeType === 1;
    }
    node2fragment(node) { // 将节点移动到内存中
        // createDocumentFragment（）建了一虚拟的节点对象，节点对象包含所有属性和方法，可以防止文件结构被破坏
        let fragment = document.createDocumentFragment(); // 创建一个文档碎片
        let firstChild; // 第一个节点
        while(firstChild = node.firstChild) { // 这边要做的事情其实是获取到挂载元素的子节点，然后追加进内存中
            // appendChild具有移动性，也就是说让firstChild往下移，可向节点的子节点列表的末尾添加新的子节点。
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
    compile(node) { // 编译内存中的dom节点
        let childNodes = node.childNodes; // 获取node节点下的子节点集合（类数组形式, object）
        [...childNodes].forEach( child => { // 使用扩展运算符转换成可迭代的格式
            if (this.isElementNode(child)) {  // 元素节点
                this.complierElement(child);
                // 如果是元素的话，需要遍历其子节点
                this.compile(child)
            } else { // 属性节点
                this.complierText(child);
            }
        })
    }
    // 编译元素 也就是v-model
    complierElement(node) {
        let attributes = node.attributes; // 获取到节点的属性集合，类数组
        [...attributes].forEach( attr => { // attr => type="text"
            let {name, value:expr} = attr; // 解构赋值 key、val为关键字 value重定义变量名，原先的作废
            if(this.isDirective(name)) { // 获取到v-开头的属性名，也就是特殊指令
                let [,directive] = name.split('-'); // 指令分割，通过后缀识别要调用的方法
                let [directiveName, eventName] = directive.split(':');
                CompileUtils[directiveName](node,expr, this.vm, eventName); // 这个思路很好，调用特殊指令对象内对应的方法
            }
        })
    }
    // 编译文本 也就是{{}}
    complierText(node) {
        let content = node.textContent; // 获取节点的文本内容
        if(/\{\{.+?\}\}/.test(content)) { // 正则匹配是否是{{}}结构
            // 文本节点
            CompileUtils['text'](node, content, this.vm);
        }
    }
    // 判断是不是特殊指令
    isDirective(attrName) {
        return attrName.startsWith('v-');
    }
}
CompileUtils = { // 处理特殊指令的对象
    // 解析v-model的指令
    model(node, value, vm) { // 节点、表达式，当前的实例对象
        let fn = this.updater['modelUpdater']; // 更新的函数
        new Watcher(vm, value, (newVal) => { // 给输入框加一个观察者,如果后面数据更新，会触发这个回调函数，会将新值替换原来的值
            fn(node, newVal);
        })
        // 对需要实时更新的输入框进行监听
        node.addEventListener('input', (e) => {
            let expr = e.target.value; // 获取用户输入的值
            this.setValue(vm, value, expr);
        })
        // 我们希望对实例对象中的对应值进行获取
        let expr = this.getVal(vm, value);
        fn(node, expr);
    },
    text(node, expr, vm) { // expr的数据结构 => {{a}}
        let fn = this.updater['textUpdater']; // 先取到这个方法，这样使得代码比较容易理解
        // args是一个正则替换后的数组 args[1] 就是我们想要的类似info.name这种变量
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            // 给表达式{{}} 每个都加上观察者
            new Watcher(vm, args[1], () => { // expr是{{a}}, args[1]才是我们想要的变量
                fn(node, this.getContentValue(vm, expr));
            });
            return this.getVal(vm, args[1]); // 同样可以通过这样去获取变量所对应的值
        });
        fn(node, content);
    },
    updater: {
        // 把数据插入到节点中
        modelUpdater(node, value) {
            node.value = value; // 将值赋值给节点上的值
        },
        textUpdater(node, value) {
            node.textContent = value; // 将值赋给节点上的文本内容（因为是一个文本节点）
        },
        htmlUpdater(node, value) { // xss攻击
            node.innerHTML = value;
        }
    },
    getVal(vm, value) { // 获取实例对象的对应值
        // 此时的value结构类似info.name
        // reduce() 方法接收一个函数作为累加器，数组中的每个值（从左到右）开始缩减，最终计算为一个值。
        return value.split('.').reduce((data, current) => { // 根据表达式取到对应的数据
            return data[current]; // 这就是我们想要的对应值 也就是类似vm.$data.info.name的值
        }, vm.$data);
    },
    getContentValue(vm, expr) { // 返回一个全的字符串
        // 遍历表达式，将节点下的{{}}的内容进行数据替换，然后再返还回去 
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(vm, args[1]);
        })
    },
    setValue(vm, expr, value) { // 给输入框赋值
        expr.split('.').reduce((data, current, index, arr) => {
            if (index == arr.length - 1) { // 最后一项时，遍历完成 => info.name
                data[current] = value;
            }
            return data[current];
        }, vm.$data)
    },
    on(node, expr, vm, eventName) { // v-on:click="changeName"
        node.addEventListener(eventName, (e) => {
            vm[expr].call(vm, e); // 要调用vm.methods内的对应方法
        })
    },
    html(node, expr, vm) { // expr => v-html="msg"
        let fn = this.updater['htmlUpdater']; // 先取到这个方法，这样使得代码比较容易理解
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal);
        })
        let value = this.getVal(vm, expr);
        fn(node, value);
    }
}
// 实现数据劫持
class Observe {
    constructor(data) {
        this.observer(data); // 获取到了实例对象的data对象
    }
    observer(data) { // 观察者模式
        if (data && typeof data === 'object') { // 判断data对象是否合法
            for(let key in data) { // 合法情况,通过迭代器获取到属性名跟对应的属性值 => info.name 111
                this.defineRective(data, key, data[key]);
            }
        } else {

        }
    }
    defineRective(obj, key, value) { // 替换成Object.defineProperty
        console.log(value); // {name: "111", age: 12} => 111 => 12
        this.observer(value); // 深度递归，继续对data内的对象进行拆解
        let dep = new Dep(); // 给每一个属性。都加上一个具有发布订阅的功能
        // Object.defineProperty()的作用就是直接在一个对象上定义一个新属性，或者修改一个已经存在的属性
        Object.defineProperty(obj, key, {
            get() {
                // 创建watcher时，会取到对应的内容，并且将watcher放到了全局上
                Dep.target && dep.addSubs(Dep.target);
                return value; // 获取到的值进行返回
            },
            set:(newVal) => { // 使用箭头函数的原因是为了让函数内的this可以指向上一层函数
                if (value == newVal) return; // 如果值先后相同则不进行替换，减少不必要的操作
                this.observer(newVal); // 如果是对象的话，继续进行拆解
                value = newVal; // 进行改变的值，将其进行替换
                dep.notify(); // 进行发布
            }
        })
    }
}
// 观察者模式 （发布订阅）
class Watcher {
    constructor(vm, expr, callback) {
        this.vm = vm;
        this.expr = expr;
        this.callback = callback; // 为了便于后面进行使用
        // 这边需要进行存放初始值，这是为了后面观察值是否发生改变（也就是新值不等于老值）
        this.oldVal = this.get();
    }
    get() { // 取值操作
        Dep.target = this; // 使得Dep获取到当前的this指向
        // 取值，将这个观察者跟数据关联起来
        let value = CompileUtils.getVal(this.vm, this.expr);
        Dep.target = null; // 恢复初始状态，不取消的话，任何值都会添加watcher
        return value;
    }
    update() { // 更新操作，数据发生改变则触发
        let newVal = CompileUtils.getVal(this.vm, this.expr);
        if (newVal !== this.oldVal) { // 数据发生改变
            this.callback(newVal); // 触发回调函数
        }
    }
}
class Dep{
    constructor() {
        this.subs = []; // 用来存放所有的观察者
    }
    // 订阅
    addSubs(Watcher) { // 添加watcher
        this.subs.push(Watcher);
    }
    // 通知 发布
    notify() {
        this.subs.forEach(watcher => watcher.update()); // 所有观察者进行更新操作
    }
}